Skip to content

fix(restore): keep the executable bit on custom-harness test binaries - #648

Open
glebpom wants to merge 1 commit into
kunobi-ninja:mainfrom
nervix-io:fix/restore-executable-bit-for-custom-harness-tests
Open

fix(restore): keep the executable bit on custom-harness test binaries#648
glebpom wants to merge 1 commit into
kunobi-ninja:mainfrom
nervix-io:fix/restore-executable-bit-for-custom-harness-tests

Conversation

@glebpom

@glebpom glebpom commented Jul 31, 2026

Copy link
Copy Markdown

A [[test]] harness = false target supplies its own main, so cargo
invokes rustc with neither --test nor --crate-type. Its extensionless
output therefore classifies as Other("rustc:unknown"), whose link
strategy is Hardlink, and the restore path derived the output's
permissions from that strategy alone — so no 0o755 was applied.

On a CoW filesystem the bit is lost twice over: the reflink ingest writes
the blob into a fresh temp file that set_blob_readonly leaves at 0o444,
and the reflink restore then creates the output at the process umask and
never chmods it. The restored test binary comes back non-executable and
cargo fails the run with "Permission denied (os error 13)". On tmpfs both
paths fall back to fs::copy, which preserves the source mode and hides
the whole thing.

The executable bit recorded at insert time is the reliable signal, and the
insert side already trusts it over the filename (hardlink_eligible
refuses to hardlink anything carrying a mode bit). Restore now trusts it
the same way, which also keeps executables on the independent-inode path
so a post-build strip or codesign cannot reach back into the shared blob.

Covered by unit tests in wrapper::tests, which reproduce without
depending on filesystem capabilities, and by a new end-to-end
custom_harness_test that builds the test-projects/custom-harness
fixture through the real wrapper, drops the target dir, and lets cargo
execute the restored binary. The e2e test runs on the repository's
filesystem rather than TMPDIR, since tmpfs masks the failure.

A `[[test]] harness = false` target supplies its own `main`, so cargo
invokes rustc with neither `--test` nor `--crate-type`. Its extensionless
output therefore classifies as `Other("rustc:unknown")`, whose link
strategy is `Hardlink`, and the restore path derived the output's
permissions from that strategy alone — so no `0o755` was applied. The
restored test binary comes back non-executable and cargo fails the run
with "Permission denied (os error 13)".

The resulting mode depends on which materialization path runs, and only
one of them preserves the bit by accident:

  reflink   cache+target on one CoW filesystem   fresh file at the umask
  hardlink  one filesystem, copy-ingested blob   blob's 0o555 — survives
  hardlink  one filesystem, reflink-ingested     blob's 0o444
  copy      cache and target on different fs     0o644, hardcoded

So this is not specific to any filesystem: reflink-capable volumes (btrfs,
XFS with reflink=1, ZFS >= 2.2, bcachefs, APFS) hit it, and so does any
setup whose cache and build directories live on separate filesystems, ext4
included. The one configuration that works is a same-filesystem hardlink
from a copy-ingested blob, which is what a plain ext4 checkout produces —
and why this went unnoticed.

The executable bit recorded at insert time is the reliable signal, and the
insert side already trusts it over the filename (`hardlink_eligible`
refuses to hardlink anything carrying a mode bit). Restore now trusts it
the same way, which also keeps executables on the independent-inode path
so a post-build `strip` or codesign cannot reach back into the shared blob.

Covered by unit tests in `wrapper::tests`, which assert the contract
directly without depending on filesystem behaviour, and by a new
end-to-end `custom_harness_test` that builds the
`test-projects/custom-harness` fixture through the real wrapper, drops the
target dir, and lets cargo execute the restored binary. That test runs on
the repository's filesystem rather than TMPDIR, which would otherwise land
in the one blind spot above.
@glebpom
glebpom force-pushed the fix/restore-executable-bit-for-custom-harness-tests branch from f1a53d8 to eda739c Compare July 31, 2026 17:07
@glebpom
glebpom marked this pull request as ready for review July 31, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant